/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @author Sergey I. Salishev
 */
package org.apache.cgiservlet;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentHashMap;

import org.jruby.Ruby;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyString;
import org.jruby.RubyInstanceConfig.LoadServiceCreator;
import org.jruby.ast.Node;
import org.jruby.ast.executable.Script;
import org.jruby.compiler.ASTCompiler;
import org.jruby.compiler.ASTInspector;
import org.jruby.compiler.impl.StandardASMCompiler;
import org.jruby.exceptions.JumpException;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.ExternalScript;
import org.jruby.runtime.load.LoadService;
import org.jruby.runtime.load.LoadServiceResource;
import org.jruby.util.JRubyClassLoader;
import org.jruby.util.JavaNameMangler;

/**
 * Load service implement
 *
 */
public class RubyCachedLoadServiceCreator implements LoadServiceCreator {
	
	private static final Field resourceAccessor;
	
	static {
		try {
			resourceAccessor = ExternalScript.class.getDeclaredField("resource");
			resourceAccessor.setAccessible(true);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	private final ConcurrentHashMap<String, CacheEntry> cache =
		new ConcurrentHashMap<String, CacheEntry>();

	@Override
	public LoadService create(Ruby runtime) {
		return new CacheService(runtime);
	}
	
	static class CacheEntry {
		final String filename;
		final Class<? extends Script> scriptClass;
		
		public CacheEntry(String filename, Class<? extends Script> scriptClass) {
			super();
			this.filename = filename;
			this.scriptClass = scriptClass;
		}			
	}

	//XXX: duplicates part of the LoadService logic to get access to intermediate
	//results of resource search
	class CacheService extends LoadService {

		public CacheService(Ruby runtime) {
			super(runtime);
		}
		
		@SuppressWarnings("unchecked")
		public boolean smartLoad(final String file) {
			CacheEntry entry = cache.get(file);
			
			if (entry == null) {
				SearchState state;
		        try {
		            state = findFileForLoad(file);
		        } catch (AlreadyLoaded al) {
		            return false;
		        }
		        if (state.library instanceof ExternalScript) {
		        	ExternalScript library = (ExternalScript) state.library;
		        	String filename = getResource(library).getURL().getPath();
		        	entry = new CacheEntry(file, compile(filename));
		        	cache.putIfAbsent(file, entry);
		        } else {
		        	return super.smartLoad(file);
		        }		        
			}
			RubyString loadNameRubyString = RubyString.newString(runtime, file);
			synchronized (loadedFeaturesInternal) {
                if (loadedFeaturesInternal.contains(loadNameRubyString)) {
                    return false;
                } else {
                	loadedFeaturesInternal.add(loadNameRubyString);
                }
            }
			Script script;
			try {
				script = entry.scriptClass.newInstance();
			} catch (Exception e) {
				RaiseException re = runtime.newLoadError("Script class error -- " + file);
	            re.initCause(e);
	            throw re;
			}
			loadScript(entry.filename, script);
			return true;
	    }

		//XXX: ugly hack to get resource from private field
		private LoadServiceResource getResource(ExternalScript library) {
			try {
				return (LoadServiceResource) resourceAccessor.get(library);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}			
		}

		private Class<? extends Script> compile(String filename) {
			InputStream in;
			try {
				in = new FileInputStream(filename);
			} catch (IOException e) {
	            throw runtime.newIOErrorFromException(e);
	        }
            Node scriptNode = runtime.parseFile(in, filename, null);            
            return compileNode(scriptNode, runtime.getJRubyClassLoader());
		}
		
	    @SuppressWarnings("unchecked")
		private Class<? extends Script> compileNode(Node node, JRubyClassLoader classLoader) {
	        RubyInstanceConfig config = runtime.getInstanceConfig();
            String filename = node.getPosition().getFile();
            String classname = JavaNameMangler.mangledFilenameForStartupClasspath(filename);
            ASTInspector inspector = new ASTInspector();
            inspector.inspect(node);
            StandardASMCompiler asmCompiler = new StandardASMCompiler(classname, filename);
            ASTCompiler compiler = config.newCompiler();
            compiler.compileRoot(node, asmCompiler, inspector, true, false);

            try {
                return (Class<? extends Script>) asmCompiler.loadClass(classLoader);	            
	        } catch (Exception e) {
				RaiseException re = runtime.newLoadError("Script compile error -- " + filename);
	            re.initCause(e);
	            throw re;	        	
	        }
	    }
		
		public void loadScript(String filename, Script script) {
	        IRubyObject self = runtime.getTopSelf();
	        ThreadContext context = runtime.getCurrentContext();
	        String file = context.getFile();
	        
	        try {
	            runtime.secure(4); /* should alter global state */

	            context.setFile(filename);
	            context.preNodeEval(runtime.getObject(), self, filename);

	            if (script == null) {
	                System.err.println("Error, could not compile; pass -J-Djruby.jit.logging.verbose=true for more details");
	            }
                script.load(context, context.getFrameSelf(), IRubyObject.NULL_ARRAY, Block.NULL_BLOCK);
	        } catch (JumpException.ReturnJump rj) {
	            return;
	        } finally {
	            context.postNodeEval();
	            context.setFile(file);
	        }
	    }
	}

}
